Repository navigation
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #412 +/- ##
==========================================
+ Coverage 92.49% 92.52% +0.02%
==========================================
Files 31 31
Lines 3146 3210 +64
==========================================
+ Hits 2910 2970 +60
- Misses 236 240 +4 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
This is how benchmark results would change (along with a 95% confidence interval in relative change) if 2a4ef85 is merged into master:
|
1.
|
| case | N | K | nonzero | one (s) | ten (s) |
|---|---|---|---|---|---|
| own-best observations | 400 | 50 | 50 | 0.38 | 0.05 |
| own-best observations | 800 | 200 | 130 | 2.84 | 0.32 |
| own-best observations | 1600 | 400 | 146 | 6.96 | 0.94 |
| independent normal | 1000 | 10000 | 129 | 22.1 | 2.7 |
| shared row effects | 5000 | 20 | 20 | 0.55 | 0.09 |
The weights of the two versions differ by at most 1.6e-10 in these cases, and both are within the bound N * 1e-8. On the 560 test inputs from item 1 there are again no warnings. With exactly identical models, the weight can now be split between the copies, because they can be added in the same iteration; the log score is the same. A new test checks that max_add = 10 and max_add = 1 give the same weights when more than ten weights are nonzero.
3. R/loo_model_weights.R:72 (documentation)
Yes, the code adds the model with the largest g_k, which is the gradient of the log score. Changed to your text. I also added that the function gives a warning if it cannot reach the bound.
4. R/loo_model_weights.R:405 (neg_log_score argument)
Changed. stacking_neg_log_score() is now an internal helper, and stacking_newton() calls it directly. stacking_log_g() is a new helper for the log(g_k) computation, which the outer loop and the warning use.
Fixes #119.
This PR was assisted by Claude.
Summary
stacking_weights()optimized the weights withstats::constrOptim(), a log-barrier method with an inner BFGS. It is slow for many models, as #119 reports (more than 10 minutes for 10 000 models). It also often stops far from the optimum:constrOptim()reports convergence after 2 or 3 outer iterations, and the barrier term keeps the weights away from zero. loo gave no warning.This PR finds the weights with an active-set Newton method that uses only base R and matrixStats.
@yao-yl @jgabry, the stacking weights change for many inputs, so I would like your review.
How far master was from the optimum
For stacking with fixed predictive densities, a bound shows how far any weights are from the optimum. With
p_i = sum_k w_k exp(lpd_ik)andg_k = mean_i exp(lpd_ik) / p_i, Jensen's inequality gives that the log score ofwis at mostN * log(max_k g_k)below its maximum. At the optimum,g_k = 1for every model with positive weight andg_k <= 1for the others (Lindsay, 1983). The same bound appears for log-optimal portfolios (Cover, 1984), which is the same optimization problem.I simulated pointwise log predictive densities for N observations and K normal models, 50 data sets per cell, and computed this bound for the master weights:
The gap is in units of the log score. A weight difference near 1 means that master put the weight on a different model than the optimum. Larger iteration limits for
constrOptim()reduce the gap but do not remove it.The old code could also give a non-zero weight to a clearly worse model. The optimization starts from equal weights, and when it stops early, models whose optimal weight is zero keep part of their starting weight. With 10 models, master gave more than 0.01 weight to a model with optimal weight zero in 54 of 100 simulated data sets for N = 100, and in 70 of 100 for N = 1000. In 27 and 57 of these, that model's elpd was more than 2 SE below the best model, and the total weight on such models was up to 0.89. With 3 or 5 models this happened in 2 of 400 data sets.
Method
Stacking with fixed predictive densities is the maximum likelihood estimate of mixture proportions with fixed components.
stacking_weights()now calls the internalstacking_weights_active_set(), a variant of the constrained Newton method (CNM) of Wang (2007):g_kfor all models. Stop whenmax_k log(g_k)is at most1e-8, so the log score is at mostN * 1e-8below its maximum. Otherwise add the model with the largestg_kand go to step 2.CNM adds all local maxima of the gradient function at each iteration and solves the quadratic approximation with a least-squares formulation. This implementation adds one model per iteration and solves the KKT system directly, which is enough here because the optimal weights are usually sparse. Each check over all models costs one pass over the
N x Kmatrix.Over 1800 simulated data sets (N from 20 to 1000, K from 2 to 50), the largest gap bound with this PR is 9.3e-7. Two other approaches were not good enough. A softmax parametrization with
optim(method = "L-BFGS-B")is fast but stalls with gaps from 0.1 to 3, because the gradient of a weight is proportional to the weight. EM for mixture weights (Cover's multiplicative update), also with SQUAREM acceleration, reaches the optimum but took more than 60 s for 1000 models.?loo_model_weightsdescribes the method in Details and cites Wang (2007) and Lindsay (1983).References:
API change
optim_methodandoptim_controlofstacking_weights()andloo_model_weights()no longer have an effect. Their defaults are nowNULL. If a user supplies them, a warning says that they are deprecated and ignored. They can be removed in a later release.The stacking weights change for many inputs, also with 3 models, because the new weights are at the optimum. For the 3-model test data in
test_model_weighting.R, the weights change from 0.203658, 0.796342, 3.6e-8 (gap bound 3.8e-4) to 0.203608, 0.796392, 0 (gap bound 2.4e-13). Please add a NEWS entry; I suggest a text below.exp_diff_over_exp()inR/helpers.Rcomputed the gradient of the old objective and has no caller after this change. This PR removes it and its tests.Tests
tests/testthat/test_model_weighting.R:stacking_weights gives expected resultcompared with values fromconstrOptim(). It now checks the gap bound and compares with an EM solution of the same data.N * 1e-8and sparse weights.-Infentries), a duplicated model, and identical models.stacking_weights()andloo_model_weights(), and the same weights with and without the deprecated arguments.With the master
R/loo_model_weights.R, the new and changed tests give 10 failed expectations. With this PR,devtools::test()withNOT_CRAN=truepasses all 1269 tests and gives no warnings (R 4.6.1, testthat 3.3.2, matrixStats 1.5.0, Linux x86_64).